package cmd

import (
	"bufio"
	"context"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/jedib0t/go-pretty/v6/table"

	"fileverify/log"

	"golang.org/x/sync/errgroup"
)

// Run 启动命令行程序
func Run() error {
	// 初始化日志文件
	log, outErr := log.NewLogFile(outF.Get()) // 初始化日志文件
	if outErr != nil {
		return fmt.Errorf("无法打开日志文件: %v", outErr)
	}
	defer func() {
		// 捕获panic
		if r := recover(); r != nil {
			log.WriteLog(fmt.Sprintf("程序发生panic: %v", r))

			// 关闭日志文件
			if log != nil {
				log.Close()
			}

			os.Exit(1)
		}

		// 关闭日志文件
		if log != nil {
			log.Close()
		}

		os.Exit(0)
	}()

	// 检查fileF.Get()指定的文件是否存在
	if fileF.Get() != "" {
		// 获取info信息
		info, err := os.Lstat(fileF.Get())
		if err != nil {
			// 检查权限错误
			if os.IsPermission(err) {
				log.WriteLog(fmt.Sprintf("没有权限访问文件: %v", err))

				return fmt.Errorf("没有权限访问文件: %v", err)
			}

			// 检查路径不存在错误
			if os.IsNotExist(err) {
				log.WriteLog(fmt.Sprintf("文件不存在: %v", err))

				return fmt.Errorf("文件不存在: %v", err)
			}

			return fmt.Errorf("无法访问的文件: %v", err)
		}

		// 检查是否是文件
		if !info.Mode().IsRegular() {
			log.WriteLog(fmt.Sprintf("%s不是一个文件", fileF.Get()))

			return fmt.Errorf("%s不是一个文件", fileF.Get())
		}
	} else {
		return fmt.Errorf("未指定校验文件, 例如: -f <file>")
	}

	// 检查dirF.Get()指定的目录是否存在
	if dirF.Get() != "" {
		// 获取info信息
		info, err := os.Lstat(dirF.Get())
		if err != nil {
			// 检查权限错误
			if os.IsPermission(err) {
				log.WriteLog(fmt.Sprintf("没有权限访问目录: %v", err))

				return fmt.Errorf("没有权限访问目录: %v", err)
			}

			// 检查路径不存在错误
			if os.IsNotExist(err) {
				log.WriteLog(fmt.Sprintf("目录不存在: %v", err))

				return fmt.Errorf("目录不存在: %v", err)
			}

			return fmt.Errorf("无法访问的目录: %v", err)
		}

		// 检查是否是目录
		if !info.IsDir() {
			log.WriteLog(fmt.Sprintf("%s不是一个目录", dirF.Get()))

			return fmt.Errorf("%s不是一个目录", dirF.Get())
		}
	} else {
		return fmt.Errorf("未指定对比目录, 例如: -d <dir>")
	}

	// 清理fileF路径
	if setErr := fileF.Set(filepath.Clean(fileF.Get())); setErr != nil {
		log.WriteLog(fmt.Sprintf("无法清理文件路径: %v", setErr))

		return fmt.Errorf("无法清理文件路径: %v", setErr)
	}

	// 清理dirF路径
	if setErr := dirF.Set(filepath.Clean(dirF.Get())); setErr != nil {
		log.WriteLog(fmt.Sprintf("无法清理目录路径: %v", setErr))

		return fmt.Errorf("无法清理目录路径: %v", setErr)
	}

	// 根据fileF的.前缀判断itemType
	var itemType string
	itemTypeTmp := filepath.Base(fileF.Get())

	// 检查是否为受支持的itemType
	if strings.Contains(itemTypeTmp, ".") {
		itemType = strings.Split(itemTypeTmp, ".")[0]
	} else {
		return fmt.Errorf("无法识别的itemType: %s, 请检查校验文件名格式 %s", strings.Split(itemTypeTmp, ".")[0], fileF.Get())
	}

	// 检查是否为受支持的itemType
	if _, ok := CoreCheckItems[itemType]; !ok {
		log.WriteLog(fmt.Sprintf("不支持的itemType: %s", itemType))

		return fmt.Errorf("不支持的itemType: %s", itemType)
	}

	// 读取校验项到map
	CheckItems, readFileErr := readCheckItemsFromFile(log)
	if readFileErr != nil {
		return fmt.Errorf("无法读取校验文件: %v", readFileErr)
	}

	// 切换到工作目录
	if dirF.Get() != "" {
		// 切换到工作目录
		workDirErr := os.Chdir(dirF.Get())
		if workDirErr != nil {
			log.WriteLog(fmt.Sprintf("无法切换到工作目录: %v", workDirErr))

			return fmt.Errorf("无法切换到工作目录: %v", workDirErr)
		}
		log.WriteLog(fmt.Sprintf("切换到工作目录: %s", dirF.Get()))
	}

	// 读取被校验目录下的文件到切片
	filePaths, getErr := GetAllFilesFromCoreItems(itemType)
	if getErr != nil {
		return fmt.Errorf("无法读取目录: %v", getErr)
	}

	// 判断是否为空
	if len(filePaths) == 0 {
		return fmt.Errorf("在读取对比目录时没有找到任何文件")
	}

	// 获取对比目录切片里的md5到map
	replaceMap, getMd5Err := calculateFileHashes(filePaths)
	if getMd5Err != nil {
		return fmt.Errorf("无法获取md5: %v", getMd5Err)
	}

	// 比较基准mapA和对比mapB, 并输出差异
	compareMaps(CheckItems, replaceMap, log)

	log.WriteLog("校验完成")
	fmt.Println("校验完成")

	return nil
}

// 用于读取校验文件内的校验项到map的函数
func readCheckItemsFromFile(log *log.LogFile) (map[string]string, error) {
	// 创建一个新的映射，用于存储替换后的路径
	replaceMap := make(CheckItems)

	// 打开校验文件
	checkFileRead, openErr := os.OpenFile(fileF.Get(), os.O_RDONLY, 0644)
	if openErr != nil {
		return nil, fmt.Errorf("无法打开校验文件: %v", openErr)
	}
	defer checkFileRead.Close()

	// 解析校验文件内容
	scanner := bufio.NewScanner(checkFileRead)

	// 定义一个计数器
	var lineCount int

	// 逐行读取校验文件
	for scanner.Scan() { // 逐行读取
		line := scanner.Text() // 获取当前行的文本

		lineCount++ // 计数器加 1

		// 如果当前行是空行或以#开头，则跳过
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		// 解析校验文件中的哈希值和文件路径
		parts := strings.Fields(line) // 按空格分割

		// 获取哈希值
		expectedHash := parts[0]

		// 获取文件路径, 从第二个元素开始到结尾
		filePath := strings.Join(parts[1:], " ")

		// 如果哈希值或文件路径为空，则跳过
		if expectedHash == "" || filePath == "" {
			log.WriteLog(fmt.Sprintf("校验文件格式错误, 文件 %s 的第 %d 行, %s\r\n", fileF.Get(), lineCount, line))

			continue
		}

		// 统一路径格式
		filePath = filepath.ToSlash(filePath)

		// 检查是否被排除
		fileName := filepath.Base(filePath)
		if !IsExcluded(fileName) {
			// 存储到 map 中
			replaceMap[filePath] = expectedHash
		}

		// 存储到 map 中
		//replaceMap[filePath] = expectedHash
	}

	// 检查是否有错误发生
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("读取校验文件时出错: %v", err)
	}

	// 检查map是否为空
	if len(replaceMap) == 0 {
		return nil, fmt.Errorf("校验文件内没有可获取的项")
	}

	return replaceMap, nil
}

// 用于根据文件路径切片获取文件的md5到map的函数
// 参数: filePaths - 文件路径切片
// 返回: 文件路径切片和错误信息
func calculateFileHashes(filePaths []string) (map[string]string, error) {
	// 创建一个新的映射，用于存储替换后的路径
	replaceMap := make(CheckItems)
	resultChan := make(chan struct {
		path string
		hash string
		err  error
	}, len(filePaths))

	// 创建errgroup
	g, ctx := errgroup.WithContext(context.Background())

	// 设置并发限制
	if jobF.Get() == -1 {
		// 设置并发限制为CPU核数的两倍
		g.SetLimit(runtime.NumCPU() * 2)
	}
	if jobF.Get() <= 0 {
		// 线程数小于等于0，则设置为1
		g.SetLimit(1)
	}
	if jobF.Get() > 20 {
		// 线程数大于20，则设置为20
		g.SetLimit(20)
	}

	// 启动单独的goroutine处理通道结果
	done := make(chan struct{})
	go func() {
		// 处理通道结果
		for res := range resultChan {
			if res.err != nil {
				continue
			}

			// 存储到 map 中
			replaceMap[res.path] = res.hash
		}
		close(done)
	}()

	// 遍历文件路径切片
	for _, filePath := range filePaths {
		filePath := filePath // 创建局部变量, 避免闭包引用循环变量
		g.Go(func() error {
			select {
			case <-ctx.Done():
				return ctx.Err()
			default:
				// 计算文件哈希值
				hash, err := checksum(filePath, typeF.Get())
				resultChan <- struct {
					path string
					hash string
					err  error
				}{filePath, hash, err}
				return err // 返回错误
			}
		})
	}

	// 等待所有goroutine完成
	waitErr := g.Wait()
	close(resultChan)
	<-done // 等待结果处理完成

	if waitErr != nil {
		return nil, waitErr
	}

	return replaceMap, nil
}

// GetAllFilesFromCoreItems 使用filepath.WalkDir高性能遍历CoreCheckItems中的文件
// 参数: itemType - CoreCheckItems中的key，如"jointos"
// 返回: 文件路径切片和错误信息
func GetAllFilesFromCoreItems(itemType string) ([]string, error) {
	var allFiles []string

	// 获取指定类型的校验项路径
	patterns, ok := CoreCheckItems[itemType]
	if !ok {
		// 返回一个空切片和nil错误，表示没有找到对应的校验项
		return []string{}, fmt.Errorf("未找到类型为 %s 的校验项", itemType)
	}

	// 遍历每个路径
	for pattern := range patterns {
		// 处理目录路径（以/结尾的路径）
		if strings.HasSuffix(pattern, "/") {
			walkErr := filepath.WalkDir(pattern, func(path string, d fs.DirEntry, err error) error {
				if err != nil {
					return err
				}

				// 跳过路径本身
				if path == pattern {
					return nil
				}

				if !d.IsDir() {
					path = filepath.ToSlash(path)
					shouldExclude := false

					// 检查所有排除规则
					for excludePattern, isExclude := range patterns {
						if !isExclude { // 只处理排除规则
							match, _ := filepath.Match(excludePattern, path)
							if match {
								shouldExclude = true
								break
							}
						}
					}

					// 如果不应该排除，则添加到结果列表中
					fileName := filepath.Base(path)
					if !shouldExclude && !IsExcluded(fileName) {
						allFiles = append(allFiles, path)
					}
				}

				return nil
			})
			if walkErr != nil {
				return nil, fmt.Errorf("遍历目录时出错: %v", walkErr)
			}
		} else {
			// 处理通配符路径
			matches, globErr := filepath.Glob(pattern)
			if globErr != nil {
				return nil, fmt.Errorf("匹配文件时出错: %v", globErr)
			}

			// 检查是否有匹配的文件
			if len(matches) == 0 {
				continue
			}

			// 统一路径格式并检查排除列表
			for i := range matches {
				// 统一路径格式
				matches[i] = filepath.ToSlash(matches[i])

				// 检查文件是否匹配任何排除规则
				shouldExclude := false
				for excludePattern, exclude := range patterns {
					if !exclude { // 只检查值为false的排除规则
						matched, _ := filepath.Match(excludePattern, matches[i])
						if matched {
							shouldExclude = true
							break
						}
					}
				}

				// 如果不应该排除，则添加到结果列表中
				fileName := filepath.Base(matches[i])
				if !shouldExclude && !IsExcluded(fileName) {
					allFiles = append(allFiles, matches[i])
				}
			}
		}
	}

	return allFiles, nil
}

// 用于比较两个map的函数
// mapA: 基准map
// mapB: 待比较map
func compareMaps(mapA, mapB CheckItems, log *log.LogFile) {
	// 初始化统计计数器
	sameCount := 0          // 相同文件计数器
	diffCount := 0          // 不同文件计数器
	onlyCheckFileCount := 0 // 校验文件计数器
	onlyDirFileCount := 0   // 对比文件计数器

	var hasCompared bool // 新增标志位，表示是否有进行过比较

	// 比较相同文件名的文件
	var sameNameCount int

	log.WriteLog(fmt.Sprintf("正在检查%s校验值不一致的文件...", typeF.Get()))

	// 遍历校验文件中的文件
	for fullPathA, hashValueA := range mapA {
		// 统一路径格式
		normalizedPathA := filepath.ToSlash(fullPathA)

		if hashValueB, ok := mapB[normalizedPathA]; ok {
			hasCompared = true // 标记已进行过比较

			// 比较两个文件的校验值
			if hashValueA != hashValueB {
				diffCount++     // 不同文件计数器加 1
				sameNameCount++ // 相同文件名计数器加 1

				// 输出结果
				log.WriteLog(fmt.Sprintf("该文件校验值不一致: %s", normalizedPathA))

			} else {
				sameCount++ // 相同文件计数器加 1
			}

			delete(mapA, fullPathA)       // 从 mapA 中移除已比较的文件
			delete(mapB, normalizedPathA) // 从 mapB 中移除已比较的文件
		}
	}

	// 根据比较结果输出提示
	if hasCompared {
		if sameCount == 0 && diffCount == 0 {
			log.WriteLog("无匹配文件")
		}
	} else {
		log.WriteLog("无匹配文件")
	}

	// 检查仅存在于校验文件中的文件
	log.WriteLog("正在检查被校验目录中不存在的文件...")

	onlyCheckFileCount = len(mapA)
	var onlyCheckFileCountDisplay int
	for fullPathA := range mapA {
		onlyCheckFileCountDisplay++
		log.WriteLog(fmt.Sprintf("该文件不存在于被校验目录: %s", fullPathA))
	}
	if onlyCheckFileCountDisplay == 0 {
		log.WriteLog("无匹配文件")
	}

	// 检查仅存在于目录中的文件
	log.WriteLog("正在检查被校验目录中的额外文件...")

	onlyDirFileCount = len(mapB)
	var onlyDirFileCountDisplay int
	for fullPathB := range mapB {
		onlyDirFileCountDisplay++
		log.WriteLog(fmt.Sprintf("该文件额外存在于被校验目录: %s", fullPathB))
	}
	if onlyDirFileCountDisplay == 0 {
		log.WriteLog("无匹配文件")
	}

	// 输出统计结果到表格
	t := table.NewWriter()
	t.AppendHeader(table.Row{"问题类型", "数量"})
	t.AppendRows([]table.Row{
		{fmt.Sprintf("%s相同文件", typeF.Get()), sameCount},
		{fmt.Sprintf("%s不匹配文件", typeF.Get()), diffCount},
		{"缺失文件", onlyCheckFileCount},
		{"额外文件", onlyDirFileCount},
		{"问题总数", diffCount + onlyCheckFileCount + onlyDirFileCount},
	})
	t.SetStyle(table.StyleLight)
	tableString := t.Render()

	// 匹配\n替换成\r\n
	tableString = strings.ReplaceAll(tableString, "\n", "\r\n")

	log.WriteLog("统计结果:\r\n" + tableString)
}
